<?php

/*
 * This script belongs to Aspect PHP.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * This script is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHAN-
 * TABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
 * General Public License for more details.
 */

/**
 * The aspect container for Aspect PHP
 *
 * @package    aspectPhp
 * @subpackage lib
 * @author     gboisse
 */
class aspectContainer {

    /** @var aspectContainer */
    protected static $instance;
    /** @var array An array of pointcuts to be placed before method calls */
    protected $aroundPointcuts;
    /** @var array An array of pointcuts to be placed before method calls */
    protected $beforePointcuts;
    /** @var array An array of pointcuts to be placed after method calls */
    protected $afterReturningPointcuts;
    /** @var array An array of pointcuts to be placed after method throws exception */
    protected $afterThrowingPointcuts;
    /** @var array An array with the instances of the aspect classes */
    protected $aspectClasses = array();

    /**
     * Creates a new aspect container
     *
     * @throws LogicException if two aspectContainer are instanciated
     */
    public function __construct() {

        if (isset(self::$instance)) {
            throw new LogicException('Cannot instantiate two aspect containers!');
        }

        $this->aroundPointcuts = array();
        $this->beforePointcuts = array();
        $this->afterReturningPointcuts = array();
        $this->afterThrowingPointcuts = array();

        self::$instance = $this;
    }

    /**
     * Retrieves the current aspect container instance
     *
     * @return aspectContainer The current aspect container instance
     */
    public static function getInstance() {

        if (isset(self::$instance)) {
            return self::$instance;
        }

        throw new RuntimeException('No aspect container instanciated!');
    }

    /**
     * Allows to add logic around a method call
     *
     * @param string $pointcutPattern The method on which to add behavior
     * @param string $aspectClassName The name of the aspect class which has the advice
     * @param string $adviceMethodName The name of the advice
     */
    public function doAround($pointcutPattern, $aspectClassName, $adviceMethodName) {

        $this->aroundPointcuts[] = $this->createPointcut($pointcutPattern, $aspectClassName, $adviceMethodName);
    }

    /**
     * Allows to add logic before a method is called on the instance
     *
     * @param string $pointcutPattern The method on which to add behavior
     * @param string $aspectClassName The name of the aspect class which has the advice
     * @param string $adviceMethodName The name of the advice
     */
    public function doBefore($pointcutPattern, $aspectClassName, $adviceMethodName) {

        $this->beforePointcuts[] = $this->createPointcut($pointcutPattern, $aspectClassName, $adviceMethodName);
    }

    /**
     * Allows to add logic after a method is called on the instance or an
     * exception is thrown by this method
     *
     * @param string $pointcutPattern The method on which to add behavior
     * @param string $aspectClassName The name of the aspect class which has the advice
     * @param string $adviceMethodName The name of the advice
     */
    public function doAfter($pointcutPattern, $aspectClassName, $adviceMethodName) {

        $pointcut = $this->createPointcut($pointcutPattern, $aspectClassName, $adviceMethodName);
        $this->afterReturningPointcuts[] = $pointcut;
        $this->afterThrowingPointcuts[] = $pointcut;
    }

    /**
     * Allows to add logic after a method is called on the instance
     *
     * @param string $pointcutPattern The method on which to add behavior
     * @param string $aspectClassName The name of the aspect class which has the advice
     * @param string $adviceMethodName The name of the advice
     */
    public function doAfterReturning($pointcutPattern, $aspectClassName, $adviceMethodName) {

        $this->afterReturningPointcuts[] = $this->createPointcut($pointcutPattern, $aspectClassName, $adviceMethodName);
    }

    /**
     * Allows to add logic after a method throws an exception
     *
     * @param string $pointcutPattern The method on which to add behavior
     * @param string $aspectClassName The name of the aspect class which has the advice
     * @param string $adviceMethodName The name of the advice
     */
    public function doAfterThrowing($pointcutPattern, $aspectClassName, $adviceMethodName) {

        $this->afterThrowingPointcuts[] = $this->createPointcut($pointcutPattern, $aspectClassName, $adviceMethodName);
    }

    /**
     * Retrieves the around pointcuts from the aspect container
     *
     * @return array The around pointcuts
     */
    public function getAroundPointcuts() {

        return $this->aroundPointcuts;
    }

    /**
     * Retrieves the before pointcuts from the aspect container
     *
     * @return array The before pointcuts
     */
    public function getBeforePointcuts() {

        return $this->beforePointcuts;
    }

    /**
     * Retrieves the afterReturning pointcuts from the aspect container
     *
     * @return array The afterReturning pointcuts
     */
    public function getAfterReturningPointcuts() {

        return $this->afterReturningPointcuts;
    }

    /**
     * Retrieves the afterThrowing pointcuts from the aspect container
     *
     * @return array The afterThrowing pointcuts
     */
    public function getAfterThrowingPointcuts() {

        return $this->afterThrowingPointcuts;
    }

    /**
     * Retrieves the instance of the requested aspect class
     *
     * @param string $className The requested aspect class
     * @return mixed The instance of the requested aspect class
     */
    public function getAspectClassInstance($className) {
        if (isset($this->aspectClasses[$className])) {
            return $this->aspectClasses[$className];
        }

        return $this->aspectClasses[$className] = new $className();
    }

    /**
     * Parses the given $pointcutPattern to be compatible with PHP preg_match
     *
     * @throws InvalidArgumentException if the given $poincutPattern is invalid
     *
     * @param  string $pointcutPattern The pointcut pattern to parse
     * @param string $aspectClassName The name of the aspect class which has the advice
     * @param string $adviceMethodName The name of the advice
     * @return aspectPointcut The aspectPointcut created from the $pointcuPattern
     */
    protected function createPointcut($pointcutPattern, $aspectClassName, $adviceMethodName) {

        preg_match_all('/^(?P<negate>!?) *(?P<type>method|class|filter) *\( *(?P<pattern>.*)\)$/', $pointcutPattern, $matches);
        if (!isset($matches['negate'][0]) || !isset($matches['type'][0]) || !isset($matches['pattern'][0])) {
            throw new InvalidArgumentException("Illegal pointcut expression '$pointcutPattern' on $adviceMethodName() from $aspectClassName!");
        }
        $negate = (empty($matches['negate'][0]) ? false : true);
        $type = $matches['type'][0];
        $pattern = trim($matches['pattern'][0]);

        switch ($type) {
            case 'method':
                preg_match_all('/^(?P<class>[\S]+)->(?P<method>[\S]+)\((?P<arguments>.*)\)$/', $pattern, $matches);
                if (!isset($matches['class'][0]) || !isset($matches['method'][0]) || !isset($matches['arguments'][0])) {
                    throw new InvalidArgumentException("Illegal method pointcut expression '$pointcutPattern' on $adviceMethodName() from $aspectClassName!");
                }
                $class = $matches['class'][0];
                $method = $matches['method'][0];
                $arguments = trim($matches['arguments'][0]);
                $arguments = aspectUtils::parseArgumentsPatternToArray($arguments);

                $pattern = '/^' . addslashes($class . '->' . $method) . '$/';
                $pointcut = new aspectPointcut($aspectClassName, $adviceMethodName, $negate, $pattern, $arguments);
                break;
            case 'class':
                preg_match_all('/^(?P<class>[\S]+)$/', $pattern, $matches);
                if (!isset($matches['class'][0])) {
                    throw new InvalidArgumentException("Illegal class pointcut expression '$pointcutPattern' on $adviceMethodName() from $aspectClassName!");
                }
                $class = $matches['class'][0];

                $pattern = '/^' . addslashes($class . '->.*') . '$/';
                $pointcut = new aspectPointcut($aspectClassName, $adviceMethodName, $negate, $pattern);
                break;
            case 'filter':
                preg_match_all('/^(?P<filter>[\w]+) *(\((?P<expression>.*)\))?$/', $pattern, $matches);
                if (!isset($matches['filter'][0]) || !isset($matches['expression'][0])) {
                    throw new InvalidArgumentException("Illegal filter pointcut expression '$pointcutPattern' on $adviceMethodName() from $aspectClassName!");
                }

                $filter = $matches['filter'][0];
                $expression = trim($matches['expression'][0]);
                if (class_exists($filter)) {
                    if (empty($expression)) {
                        $pointcut = new $filter($aspectClassName, $adviceMethodName, $negate);
                    } else {
                        $pointcut = new $filter($aspectClassName, $adviceMethodName, $negate, $expression);
                    }
                } else {
                    throw new RuntimeException("Invalid classname '$pattern' for the custom pointcut '$pointcutPattern' on the method $adviceMethodName from $aspectClassName!");
                }
                break;
            default:
                throw new InvalidArgumentException("Unknown pointcut type '$type' on $adviceMethodName() from $aspectClassName!");
                break;
        }

        if (!isset($pointcut)) {
            throw new RuntimeException("An error occured while building from the pointcut expression '$pointcutPattern' on $adviceMethodName() from $aspectClassName!");
        }
        if (!($pointcut instanceof aspectPointcutFilter)) {
            throw new LogicException("The pointcut '" . get_class($pointcut) . "' doesn't implement the abstract class aspectPoincutFilter!");
        }
        return $pointcut;
    }

}
